Introduce FundingContributionBuilder API#4516
Conversation
|
👋 Thanks for assigning @TheBlueMatt as a reviewer! |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4516 +/- ##
==========================================
+ Coverage 86.99% 87.08% +0.08%
==========================================
Files 163 161 -2
Lines 108635 109248 +613
Branches 108635 109248 +613
==========================================
+ Hits 94511 95138 +627
+ Misses 11647 11629 -18
- Partials 2477 2481 +4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
0968836 to
2da45ef
Compare
|
Leaving the explicit input support for a follow-up as this PR is large enough already. |
| .await | ||
| .map_err(|_| FundingContributionError::CoinSelectionFailed)?; | ||
|
|
||
| return Ok(FundingContribution::new( |
There was a problem hiding this comment.
Nit: unnecessary return in final expression position. Same on line 740 for the sync variant.
| return Ok(FundingContribution::new( | |
| Ok(FundingContribution::new( |
Review Summary — PR #4516: Introduce FundingContributionBuilder APINew issue posted this pass
Previously flagged issues (all still present)Critical:
Correctness: Diagnostic / DX: Code quality: Cross-cutting concerns
|
TheBlueMatt
left a comment
There was a problem hiding this comment.
I didn't look too deeply at the tests but basically LGTM.
2da45ef to
19b230b
Compare
|
Had to rebase due to a small import conflict. |
TheBlueMatt
left a comment
There was a problem hiding this comment.
needs rebase again tho
jkczyz
left a comment
There was a problem hiding this comment.
Code looks good, but I think merging some impl blocks will help the diff.
de4ccfb to
97eb0fc
Compare
97eb0fc to
fefea5c
Compare
The `holder_balance` is computed by `FundedChannel::get_holder_counterparty_balances_floor_incl_fee`, which may unexpectedly fail due to the balance either being too high or too low. These cases are highly unlikely to happen given we have validation to ensure we never enter such a state to begin with. If they were to happen, something has gone wrong with the channel and it doesn't make sense to allow splicing anyway. Therefore, we opt to make `PriorContribution::holder_balance` non-optional and return an error that the channel cannot be spliced at the moment.
This commit removes `FundingContribution::value_added` as tracking it is unnecessary -- it can just be derived from the total amount in minus total amount out minus fees.
fefea5c to
33f510c
Compare
jkczyz
left a comment
There was a problem hiding this comment.
PR description could probably be completed now.
33f510c to
fa10899
Compare
| let total_output_value = self | ||
| .outputs | ||
| .iter() | ||
| .chain(self.change_output.iter()) | ||
| .map(|txout| txout.value) | ||
| .sum::<Amount>() | ||
| .to_signed() | ||
| .expect("value_removed is validated to not exceed Amount::MAX_MONEY"); | ||
|
|
||
| let contribution_amount = value_added - value_removed; | ||
| contribution_amount | ||
| .checked_sub(unpaid_fees) | ||
| .expect("total_output_value is validated to not exceed Amount::MAX_MONEY"); |
There was a problem hiding this comment.
I see that the total input value is checked in validate_inputs, but I don't see where we validate the outputs.
There was a problem hiding this comment.
It's in validate_contribution_parameters
|
🔔 1st Reminder Hey @TheBlueMatt! This PR has been waiting for your review. |
TheBlueMatt
left a comment
There was a problem hiding this comment.
Some nits on docs but I'm happy with the logic now. Will finish a full review after lunch.
| pub fn value_added(&self) -> Amount { | ||
| self.value_added | ||
| let total_input_value = self.inputs.iter().map(|i| i.utxo.output.value).sum::<Amount>(); | ||
| let total_output_value = self.outputs.iter().map(|output| output.value).sum::<Amount>(); |
There was a problem hiding this comment.
I'm confused how this differs from net_value now - they appear to be identical, do we still need both?
There was a problem hiding this comment.
Mostly just used to preserve the value_added when resuming from a prior contribution. If we had a net-negative contribution, then this returns 0 rather than the negative value itself.
When a user requests to add value via coin-selected inputs, we should strive to fulfill their request. Allowing them to remove value from the channel is undesired as it goes against their request. While we still allow adding outputs to enabled mixed contributions, their funds must now always come from the set of coin-selected inputs, and must never draw from the channel balance resulting in a smaller added value.
This lets callers easily amend a prior contribution in place and only re-run coin selection when the new request cannot be satisfied with the existing inputs.
This results in a slight change of behavior: now these methods reuse and amend the prior contribution, as opposed to always starting from a fresh contribution, which would be the desired expected behavior by users.
fa10899 to
9f9fe58
Compare
| FeeRate::MAX, | ||
| &WalletSync::new(wallet, logger.clone()), | ||
| ) | ||
| funding_template.splice_out(outputs, feerate, FeeRate::MAX) |
There was a problem hiding this comment.
Bug: splice_out now has additive output semantics when a prior contribution exists (via with_prior_contribution). On RBF calls, the builder is seeded with the prior's outputs, then add_outputs(outputs) appends the same withdrawal again — doubling the withdrawal amount.
Trace for the RBF case:
- Prior contribution has
outputs = [TxOut { value: 546, ... }], no inputs (splice-out) - Builder seeds from prior:
outputs = [withdrawal] splice_outcallsadd_outputs([withdrawal])→outputs = [withdrawal, withdrawal]build()→amend_without_coin_selectionconstructs a splice-out with doubled outputscompute_feerate_adjustmentchecksfee + 1092 <= holder_balance— which passes (capacity > 20K sat)- Result: the RBF splice-out silently withdraws 2 × 546 = 1092 sat instead of 546 sat
The old splice_out_sync destructured the template with .. which discarded the prior contribution, so outputs were always treated as absolute. The new splice_out is additive.
Fix: use without_prior_contribution in the closure when a prior exists, or use rbf_prior_contribution_sync for the RBF case. For example:
let has_prior = funding_template.prior_contribution().is_some();
if has_prior {
funding_template.rbf_prior_contribution_sync(Some(feerate), FeeRate::MAX, wallet)
} else {
funding_template.splice_out(outputs, feerate, FeeRate::MAX)
}(The splice_in closure has the analogous issue — already flagged in a prior review pass.)
There was a problem hiding this comment.
@wpaulino Should we address this given the earlier comment?
// We conditionally splice out `MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS` only when the node
// has double the balance required to send a payment upon a `0xff` byte. We do this to
// ensure there's always liquidity available for a payment to succeed then.There was a problem hiding this comment.
Discussed offline. We'll wait for #4550 to land.
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Shouldn't we also be rejecting here if we don't have enough to meet the previously-requested target value?
TheBlueMatt
left a comment
There was a problem hiding this comment.
One comment, otherwise post-merge ACK
This PR refactors splice contribution construction around a new
FundingBuilderAPI and tightens the semantics of how splice value is funded. It replaces the olderFundingTemplatecontribution helpers with a builder flow that can reuse, amend, or replace prior contributions for fresh splices and RBF attempts.